package heap;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Heap {

	private static Heap instance;

	public static void initializeHeap(int heapsize) {
		instance = new Heap(heapsize);
	}

	public static Heap getInstance() {
		return instance;
	}

	private boolean isToSpace = true;

	private int heapSize;
	private int[] heap;
	private int nextPosition;

	private Map<String, Integer> references;

	private Heap(int heapSize) {
		super();
		this.heapSize = heapSize;
		heap = new int[heapSize];
		nextPosition = 1;
		isToSpace = true;
		references = new HashMap<String, Integer>();
	}

	public void defineT(String varName) {
		if (!references.containsKey(varName)) {

			int data[] = new int[4];
			data[0] = Types.T.getId();
			data[1] = 0; // f1 = NULL
			data[2] = 0; // f2 = NULL
			data[3] = 0; // data = 0;
			references.put(varName, add(data));
		}
	}

	public void defineS(String varName, String value) {
		// if (!references.containsKey(varName)) {

		// nie bedzie przekazywany NULL

		int[] data = new int[2 + value.length()];

		data[0] = Types.S.getId();
		data[1] = value.length();

		for (int i = 0; i < value.length(); i++) {
			data[2 + i] = value.charAt(i);
		}

		references.put(varName, add(data));
		// }
	}

	// varName1 = varName2
	public void redefine(String varName1, int varPosition2) {

		if (references.containsKey(varName1)) {
			references.put(varName1, varPosition2);
		} else {
			throw new RuntimeException("Invalid reference");
		}
	}

	public Types typeOf(String varName) {

		int pos = references.get(varName);

		if (Types.S.getId() == heap[pos]) {
			return Types.S;
		} else if (Types.T.getId() == heap[pos]) {
			return Types.T;
		} else {
			return Types.NULL;
		}
	}

	public Types typeOf(int pos) {

		if (Types.S.getId() == heap[pos]) {
			return Types.S;
		} else if (Types.T.getId() == heap[pos]) {
			return Types.T;
		} else {
			return Types.NULL;
		}
	}

	public int add(int[] data) {

		if (isRunningOutOfMemory(data.length)) {
			// GC start!
			if (isCollecting) {
				throw new RuntimeException("BRAK PAMIECI");
			} else {
				System.out.println("GCStart");
				collect();
				if (isRunningOutOfMemory(data.length))
					throw new RuntimeException("BRAK PAMIECI");
			}
		}

		int insertPos = nextPosition;
		System.arraycopy(data, 0, heap, insertPos, data.length);
		nextPosition += data.length;

		return insertPos;

	}

	private boolean isCollecting = false;

	public boolean isCollecting() {
		return isCollecting;
	}

	public void setCollecting(boolean isCollecting) {
		this.isCollecting = isCollecting;
	}

	public boolean isRunningOutOfMemory(int objectSize) {
		if (nextPosition + objectSize - 1 > getToSpaceHalfEnd())
			return true;
		else
			return false;
	}

	public void collect() {
		isCollecting = true;
		changeHalf();
		int index = getFromSpaceHalfBegin();

		while (index <= getFromSpaceHalfEnd()) {
			if (isReferenced(index)) {
				int newPosition = moveDataToToSpaceHalf(index);
				updatePosition(index, newPosition);
				index += getSize(index);
			} else {
				index++;
			}
		}
		
		//wyczyszenie
		Arrays.fill(heap, getFromSpaceHalfBegin(), getFromSpaceHalfEnd(), 0);

		isCollecting = false;
	}

	public List<String> getReferenceNames(int position) {

		List<String> result = new LinkedList<String>();

		for (Map.Entry<String, Integer> entry : references.entrySet()) {

			if (position == entry.getValue()) {
				result.add(entry.getKey());
			}
		}
		return result;
	}

	public Collection<Integer> collectT() {

		List<Integer> result = new LinkedList<Integer>();
//		Set<Integer> added = new HashSet<Integer>();
//
//		for (Map.Entry<String, Integer> entry : references.entrySet()) {
//
//			int position = entry.getValue();
//
//			if (typeOf(position).equals(Types.T)) {
//				if (!added.contains(position)) {
//					result.add(getData(position));
//				}
//			}
//		}
		
		int index = 1;

		while (index < heapSize){
			
			Types type = typeOf(index);
			
			switch (type) {
			case T:
				result.add(getData(index));
				index+=4;
				break;
			case S:
				index += getStringLength(index);
				break;
			default:
				index++;
				break;
			}
			
		}	

		return result;
	}

	public Collection<String> collectS() {

		List<String> result = new LinkedList<String>();
		
//		Set<Integer> added = new HashSet<Integer>();
//
//		for (Map.Entry<String, Integer> entry : references.entrySet()) {
//
//			int position = entry.getValue();
//
//			if (typeOf(position).equals(Types.S)) {
//				if (!added.contains(position)) {
//					result.add(getString(position));
//				}
//			}
//		}
		
		int index = 1;

		while (index < heapSize){			
			Types type = typeOf(index);
			
			switch (type) {
			case T:
				index+=4;
				break;
			case S:
				result.add(getString(index));
				index+=getSize(index);
				break;
			default:
				index++;
				break;
			}
			
		}	
		

		return result;
	}
	
	
	

	public void updatePosition(int oldPosition, int newPosition) {

		for (String ref : getReferenceNames(oldPosition)) {
			references.put(ref, newPosition);
		}

		for (Integer i : references.values()) {
			if (typeOf(i).equals(Types.T)) {
				if (getF1Position(i) == oldPosition) {
					setF1Position(i, newPosition);
				} else if (getF2Position(i) == oldPosition) {
					setF2Position(i, newPosition);
				}
			}
		}
	}

	private int moveDataToToSpaceHalf(int oldPosition) {
		int size = getSize(oldPosition);
		int newPosition = nextPosition;
		System.arraycopy(heap, oldPosition, heap, newPosition, size);
		nextPosition += size;
		return newPosition;
	}

	public int getSize(int position) {

		int size = 0;

		Types type = typeOf(position);

		switch (type) {
		case T:
			size = 4;
			break;
		case S:
			size = 2 + heap[position + 1];
			break;
		default:
			size = 0;
			break;
		}

		return size;

	}

	public boolean isReferenced(int position) {

		for (Integer i : references.values()) {
			if (position == i)
				return true;
			else if (typeOf(i).equals(Types.T)) {
				if (getF1Position(i) == position
						|| getF2Position(i) == position) {
					return true;
				}
			}
		}

		return false;
	}

	public int getPosition(String varName) {
		if (references.containsKey(varName))
			return references.get(varName);
		else
			return 0;
	}

	public int getF1Position(int tObjectPosition) {
		return heap[tObjectPosition + 1];
	}

	public int getF2Position(int tObjectPosition) {
		return heap[tObjectPosition + 2];
	}

	public int getData(int tObjectPosition) {
		return heap[tObjectPosition + 3];
	}

	public int getStringLength(int sObjectPosition) {
		return heap[sObjectPosition + 1];
	}

	public String getString(int sObjectPosition) {
		// int size = getStringLength(sObjectPosition);
		// char[] text = new char[size];
		//
		// for (int i = 0; i < size; i++) {
		// text[i] = (char) heap[sObjectPosition + 2 + i];
		// }
		// return new String(text);
		return new String(heap, sObjectPosition + 2, heap[sObjectPosition + 1]);
	}

	public String getString(String varName) {
		int position = getPosition(varName);
		return getString(position);
	}

	public int setF1Position(int tObjectPosition, int newValue) {
		return heap[tObjectPosition + 1] = newValue;
	}

	public int setF2Position(int tObjectPosition, int newValue) {
		return heap[tObjectPosition + 2] = newValue;
	}

	public int setData(int tObjectPosition, int newValue) {
		return heap[tObjectPosition + 3] = newValue;
	}

	public void overview() {
		for (Map.Entry<String, Integer> entry : references.entrySet()) {
			if (entry.getValue() != 0) {
				if (heap[entry.getValue()] == Types.T.getId())
					System.out.println(entry.getKey() + " " + entry.getValue()
							+ " [" + heap[entry.getValue()] + ", "
							+ heap[entry.getValue() + 1] + ", "
							+ heap[entry.getValue() + 2] + ", "
							+ heap[entry.getValue() + 3] + "]");
				else {
					System.out.print(entry.getKey() + " " + entry.getValue()
							+ " [");
					for (int i = 0; i < heap[entry.getValue() + 1]; i++) {
						System.out.print((char) heap[entry.getValue() + 2 + i]);
					}
					System.out.print("]\n");
				}
			} else {
				System.out.println(entry.getKey() + " NULL");
			}
		}

	}

	public int getToSpaceHalfBegin() {
		return isToSpace ? 1 : heapSize / 2 + 1;
	}

	public int getToSpaceHalfEnd() {
		return isToSpace ? heapSize / 2 : heapSize - 1;
	}

	public int getFromSpaceHalfBegin() {
		return isToSpace ? heapSize / 2 + 1 : 1;
	}

	public int getFromSpaceHalfEnd() {
		return isToSpace ? heapSize - 1 : heapSize / 2;
	}

	public void changeHalf() {

		if (isToSpace)
			isToSpace = false;
		else
			isToSpace = true;

		nextPosition = getToSpaceHalfBegin();

	}

}
